package risoe.syslab.fhnew.hw;

/*
 * Copyright (c) 2012-2014, Technical University of Denmark (DTU)
 * All rights reserved.
 * 
 * The Flexhouse 2.0 platform ("this software") is licensed under the
 * BSD 3-clause license.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   - Neither the name of DTU nor the names of its contributors may be used to
 *     endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE TECHNICAL UNIVERSITY OF DENMARK BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import net.wimpi.modbus.Modbus;
import net.wimpi.modbus.io.ModbusTCPTransaction;
import net.wimpi.modbus.msg.ReadInputRegistersRequest;
import net.wimpi.modbus.msg.ReadInputRegistersResponse;
import net.wimpi.modbus.msg.WriteSingleRegisterRequest;
import net.wimpi.modbus.net.TCPMasterConnection;
import net.wimpi.modbus.procimg.SimpleRegister;

import risoe.syslab.fhnew.config.AbstractIOConfig;
import risoe.syslab.fhnew.config.ActuatorConfig;
import risoe.syslab.fhnew.config.DaliInterfaceConfig;
import risoe.syslab.fhnew.config.SensorConfig;
import risoe.syslab.fhnew.hw.modbus.PhoenixRegister;

/**
 * Hardware Interface for communicating with hardware utilizing the DaliBus protocol over tcp.
 * @author M. Svendsen
 */
public class DaliInterface extends HardwareInterface implements ConnectionManagerInterface{
	private static final int BYTE_SCALING = 254;
	private InetAddress mInetAddress;
	private int mInReg;
	private int mOutReg;
	private HashMap<String, Integer> mAddressMap;

	private PhoenixRegister daliWordOUT1;
	private PhoenixRegister daliWordOUT2;
	private PhoenixRegister daliWordIN1;
	private PhoenixRegister daliWordIN2;

	private boolean mIsConnected = false;
	private TCPMasterConnection connection;
	private ConnectionManager mConnectionManager;
	private static boolean debug = false;

	private Executor mExecutor = Executors.newSingleThreadExecutor();

	public DaliInterface(DaliInterfaceConfig hic) {
		super(hic);
		mInetAddress = hic.getHostIPAddress();
		mInReg = hic.getInReg();
		mOutReg = hic.getOutReg();

		// Set up registers - TODO: Make this more dynamic
		//Commands to the controller
		daliWordOUT1 = new PhoenixRegister(mOutReg);
		// 0 0 0 0 TB C C C 0 0 0 0 0 0 0 0
		daliWordOUT2 = new PhoenixRegister(mOutReg + 1);
		// Y A A A A A A S C C C C C C C C if S=1
		// Y A A A A A A S D D D D D D D D if S=0


		//Response from the controller
		daliWordIN1 = new PhoenixRegister(mInReg);
		// SB K AW F TB C C C R R R R R R R R
		daliWordIN2 = new PhoenixRegister(mInReg + 1);
		// Y A A A A A A S C C C C C C C C if S=1
		// Y A A A A A A S D D D D D D D D if S=0
	}

	@Override
	protected void startInterfaceImpl(ArrayList<SensorConfig> sensors,
			ArrayList<ActuatorConfig> actuators) {
		mAddressMap = new HashMap<String, Integer>(sensors.size()+actuators.size());

		int size = sensors.size()+actuators.size();
		int offset = sensors.size();
		for (int i=0; i<size; i++){
			AbstractIOConfig device = i<sensors.size()? sensors.get(i): actuators.get(i-offset);

			String addrString = device.getInterfaceAttributes().get("addr");
			if (addrString != null){
				try{
					int addr = Integer.valueOf(addrString);
					mAddressMap.put(device.getName(), addr);
				}
				catch (NumberFormatException e){
					getLogger().logEvent(getName(), "Format Error", "Error parsing address: "+addrString+" of device: "+device);
				}
			}
			else{
				getLogger().logEvent(getName(), "Lookup Error", "Error getting address of device: "+device);
			}
		}

		getLogger().logEvent(getName(), "Initialization", "Complete");

		mConnectionManager = new ConnectionManager(this, 30000);
		mConnectionManager.startConnection();
	}

	@Override
	public boolean connect() {
		// Connect to the coupler
		try {
			connection = new TCPMasterConnection(mInetAddress);
			connection.setPort(Modbus.DEFAULT_PORT);
			connection.connect();
			return true;
		}
		catch (Exception e) {
			connection.close();
			connection = null;
		}
		return false;
	}

	@Override
	public void disconnect() {
		if (connection != null){
			connection.close();
			connection = null;
		}
	}

	@Override
	public boolean probeConnection() {
		synchronized (connection) {
			if (getRegisterValue(daliWordIN1) != -1){
				return true;
			}
		}
		return false;
	}

	@Override
	protected void stopInterfaceImpl() {
		mConnectionManager.stopConnection();
	}

	@Override
	public boolean isConnected() {
		return mIsConnected;
	}

	@Override
	public void applyActuation(Actuator actuator) {
		if (!mConnectionManager.isConnected()){
			getLogger().logEvent(getName(), "Error", "Error applying actuation - interface is not connected");
			return;
		}

		// Get the address of this actuator
		final Integer address = mAddressMap.get(actuator.getName());
		final int value = (int)(actuator.getSetpoint().value*BYTE_SCALING);
		if (address != null){
			mExecutor.execute(new Runnable() {

				@Override
				public void run() {
					synchronized (connection) {
						// Handle
						try{
							setRegisterValue(daliWordOUT2, 0x0000 | ((address & 0x3F) << 9)	| (value & 0xFF)); // Command
							setRegisterValue(daliWordOUT1, 0x0900); // Execute
							setRegisterValue(daliWordOUT1, 0x0000); // DALI bus Idle
							Thread.sleep(40);
						} catch (InterruptedException e){}
						catch(Exception e){
							getLogger().logEvent(getName(), "Error", "Error applying actuation: "+e.getMessage());
						}
					}
				}
			});
		}
		else{
			getLogger().logEvent(getName(), "Error", "Error applying actuation, address is null for device: "
					+actuator.getName());
		}
	}


	private void writeRegisterValue(PhoenixRegister register, int value){
		try{
			WriteSingleRegisterRequest write = new WriteSingleRegisterRequest(register.getAddress(), new SimpleRegister(value));
			ModbusTCPTransaction trans = new ModbusTCPTransaction(connection);
			trans.setRequest(write);
			trans.execute();
			mConnectionManager.signal();
		} catch (Exception e) {
			mConnectionManager.onDisconnected();
		}
	}

	private int readRegisterValue(PhoenixRegister register){
		try{
			ReadInputRegistersRequest read = new ReadInputRegistersRequest(register.getAddress(), register.getRegisterToRead());
			ModbusTCPTransaction trans = new ModbusTCPTransaction(connection);
			trans.setRequest(read);
			if(debug ) {System.out.println("Request: " + read.getHexMessage());}
			trans.execute();
			ReadInputRegistersResponse response = (ReadInputRegistersResponse) trans.getResponse();
			if(debug) {System.out.println("Response: " + response.getHexMessage());}
			mConnectionManager.signal();
			return response.getRegisterValue(register.getRegisterOffset());
		} catch (Exception e) {
			mConnectionManager.onDisconnected();
		}
		return -1;
	}

	public void setRegisterValue(PhoenixRegister register, int value){
		writeRegisterValue(register, value);
		if(debug){System.out.println("Set Method executed. The Value written is: " + value + " in register " + register.getAddress());}
	}

	public int getRegisterValue(PhoenixRegister register){
		int newValue = readRegisterValue(register);
		if(debug){System.out.println("Get Method executed. The Value read is: " + newValue + " in register " + register.getAddress());}
		return newValue;
	}



	/**
	 * CONFIGURATION FUNCTIONS
	 */
	// To apply the address to the single lamp: 
	// - make sure only this lamp is connected to DALI bus
	// - use this function to assign the new address to the lamp
	/**
	 * Function to configure the address of the light connected to the bus
	 *  To apply the address to the single lamp: 
	 *  - make sure only this lamp is connected to DALI bus 
	 *  - use this function to assign the new address to the lamp
	 * @param address
	 */
	public void configureAddress(int address) {
		writeDTR((address << 1) | 0x01);
		broadcastCommand(0x80);

		// readDTR();  //Read the new address - uncomment for debugging
		// Following was supposed to do the same but did not work. Investigate later
		// fh1.setRegisterValue(daliWordOUT2, 0xB700 | (address & 0xFF));
		//
		// fh1.setRegisterValue(daliWordOUT1, 0x0900); // Execute
		//
		// fh1.setRegisterValue(daliWordOUT1, 0x0100); // DALI bus Idle

	}

	//  Broadcast the command to all lamps
	public void broadcastCommand(int command) {
		setRegisterValue(daliWordOUT2, 0xFF00 | (command & 0xFF));
		setRegisterValue(daliWordOUT1, 0x0900); // Execute
		setRegisterValue(daliWordOUT1, 0x0100); // DALI bus Idle
	}

	//  Read data register value
	public void readDTR() {
		broadcastCommand(0x98);
		System.out.println("Reading DTR:");
		System.out.println(Integer.toHexString(getRegisterValue(daliWordIN1) & 0xFF));
	}

	//  Write the data to data register
	public void writeDTR(int value) {
		setRegisterValue(daliWordOUT2, 0xA300 | value);
		setRegisterValue(daliWordOUT1, 0x0900); // Execute
		setRegisterValue(daliWordOUT1, 0x0100); // DALI bus Idle
	}

	//  Read the response to the dali command
	public void readResponse() {
		System.out.println(Integer.toHexString(getRegisterValue(daliWordIN1)));
		System.out.println(Integer.toHexString(getRegisterValue(daliWordIN2)));
	}
}
